<?php

define("ROOT_TYPE_ID",1);
define("ROOT_TYPE_NAME",2);

/**
 * class Tree
 * This class is a clean implementation of a modified preorder tree traversal hierachy to relational model
 * Don't use this class directly as it won't work, extend it and set the $this->_table variable, currently
 * this class needs its own sequence per table. MPTT uses a lot of self referential parent child relationships
 * and having ids that are more or less sequential makes human reading, fixing and reconstruction much easier.
 */
class Tree {

	/**
	* This is the name of the table this tree is stored in
	* @var string
	*/
	var $_table;
	
	/**
	* This is a lookup table so that you can get a node name or parent id from its id
	* @var array
	*/
	var $_id_name;
	
	/**
	* This is a lookup table so that you can get a node idor parent id from its name
	* @var array
	*/
	var $_name_id;
	
	/**
	* This is a db abstraction object compatible with ADODB
	* @var object the constructor expects it to be available as $GLOBALS['adodb']['db']
	*/
	var $_db;

	/**
	* Flag to set root type
	* @var	int	ROOT_TYPE_ID|ROOT_TYPE_NAME
	*/
	var $_root_type;
	
	/**
	* The constructor takes a value and a flag determining if the value is the id of a the desired root node or the name
	* @param mixed $root name or id of desired root node
	* @param int $root_type optional flag indicating if $root is a name or id, defaults to id
	*/
	function Tree($root,$root_type = ROOT_TYPE_ID) {
		$this->_db = $GLOBALS['frame']['adodb']['db'];
		$this->_root = mysql_real_escape_string($root);
		$this->_root_type = (int)$root_type;
		$this->load_tree();
	}
	
	function load_tree() {
	  $root = $this->_root;
	  $tree = array();
	  $tree_tmp = array();
  	   
	  //get the left and right value of the root node
	  $sql = "SELECT * FROM " . $this->_table . " WHERE id='".$root."'";
	  
	  if ($this->_root_type == ROOT_TYPE_NAME) {
	  	$sql = "SELECT * FROM " . $this->_table . " WHERE name='".$root."'";
	  }
	  $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());   
	  $row = array('lft'=>'','rght'=>'');

	  if($result && !$result->EOF) {
	    $row = $result->fields;
	  }
	  else {
	  	$this->tree = array();
	  }

	  // start with an empty right stack
	  $right = array();

	  // now, retrieve all descendants of the root node
	  $sql = "SELECT * FROM " . $this->_table . " WHERE lft BETWEEN " . $row['lft'] . " AND " . $row['rght'] . " ORDER BY parent,name ASC;";
	  $result = $this->_db->Execute($sql);
	  $this->_id_name = array();
   
	  while ($result && !$result->EOF) {
	    $ar = array();
	    $row = $result->fields;
	    
	    //create a lookup table of id to name for every node that will end up in this tree, this is used
	    //by the array building code below to find the chain of parents for each node
	    $this->_id_name[$row['id']] = array("id" => $row['id'], "name" => $row['name'], "parent" => $row['parent']);
   		$this->_name_id[$row['name']] = array("id" => $row['id'], "value" => $row['value'], "parent" => $row['parent']);
   		
	    // only check stack if there is one
	    if (count($right)>0) {
	      // check if we should remove a node from the stack
	      while ($right[count($right)-1]<$row['rght']) {
	        array_pop($right);
	      }
	    }

		//set up necessary variables to then determine the chain of parents for each node
	    $parent = $row['parent'];
	    $loop = 0;
	    
	    //this is a string that gets evaled below to create the array representing the tree
	    $ar_string = "[\"".($row['id']) ."\"] = \$row[\"value\"]"; 
	    
	    //if parent is 0 then the node has no parents, the number of nodes in the id_name lookup always includes any nodes
	    //that could be the parent of any future node in the record set, the order is deterministic because of the algorithm
	    while($parent != 0 && $loop < count($this->_id_name)) {
		  $ar_string = "[\"" . ($this->_id_name[$parent]['id']) . "\"]" . $ar_string;
		  $loop++;
		  $parent = $this->_id_name[$parent]['parent'];
		}
		
		$ar_string = '$ar' . $ar_string . ";";
		//echo $ar_string;
		
		//now eval the string to create the tree array
		//there must be a more efficient way to do this than eval?
		eval($ar_string);
		
		//merge the evaled array with all of the already exsiting tree elements, 
		//merge recursive is used so that no keys are replaced in other words a key
		//with a specific value will not be replace but instead that value will be turned into an array
		//consisting of the previous value and the new value
		$tree = array_merge_n($tree,$ar);
      
		// add this node to the stack
		$right[] = $row['rght'];
		$result->MoveNext();
	  }
	  
	  $this->tree = $tree;
	}

	/*
	*	This function completely rebuilds a tree starting from parent to ensure that all of its preorder values 
	*	are integrous.
	*	Upside is that it fixes any kind of goofiness, downside is that it is recursive and consequently 
	*	exponentially expensive with the size of the tree.
	*	On adds and deletes the tree does dynamic updates as appropriate to maintain integrity of the algorithm,
	*	however you can still force it to do goofy things and afterwards you will need this function to fix it.
	*	If you need to do a huge number of adds or deletes it will be much faster to act directly on the db and then
	*	call this to fix the mess than to use the add and delete functions.
	*	@param int $parent id of the node you would like to rebuild all nodes below
	*	@param int $left optional proper left value of the node you are rebuilding below, then used recursively
	*/	
	function rebuild_tree($parent, $left = null) {
	  
	  //if no left is supplied assume the existing left is proper
	  if ($left == null) {
	    $sql = "SELECT lft FROM " . $this->_table . " WHERE id='" . $parent . "';";
	    $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());

	    if($result && !$result->EOF) {
	    	$left = $result->fields['lft'];	
	    }
	    else {
	    	//the node you are rebuilding below if goofed up and you didn't supply a proper value
	    	//nothing we can do so error
	    	die("Error: The node you are rebuilding from could not be found, please supply an existing node id.");	
	    }
	  }
	  // get all children of this node
	  $sql = "SELECT id FROM " . $this->_table . " WHERE parent='" . $parent . "' ORDER BY id;";
	  $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  	  
	  // the right value of this node is the left value + 1
	  $right = $left+1;
   
	  while ($result && !$result->EOF) {
		$row = $result->fields;
		// recursive execution of this function for each
		// child of this node
		// $right is the current right value, which is
		// incremented by the rebuild_tree function
		$right = $this->rebuild_tree($row['id'], $right);
		$result->MoveNext();
	  }

	  // we've got the left value, and now that we've processed
	  // the children of this node we also know the right value
	  $sql = "UPDATE " . $this->_table . " SET lft=".$left.", rght=".$right." WHERE id='".$parent."';";
	  //echo $sql . "<br>";
	  $this->_db->Execute($sql) or die("Error: $sql" . $this->_db->ErrorMsg());

	  // return the right value of this node + 1
	  return $right+1;
	} 

	
	/*
	*	Call this to add a new node to the tree
	*	@param int $parent id of the node you would like the new node to have as its parent
	*	@param string $name the name of the new node, it will be used to reference its value in the tree array
	*	@param string $value optional value this node is to contain
	*	@return int id of newly added node
	*/
	function add_node($parent_id,$name,$value="") {
   	  
   	  $sql = "SELECT * from " . $this->_table . " where name='" . $name . "'";
	  $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  
	  if ($result && !$result->EOF) {
	  	die("You cannot add a node with the name '" . $name ."' because one already exists. <br>");
	  }
   	  
	  $sql = "SELECT * from " . $this->_table . " where id = '" . $parent_id . "'";
	  $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
  	  
   	  $next_right = 0;
   	  
	  if ($result && !$result->EOF) {
	    $next_right = $result->fields['rght']; 
	  }
   
	  $sql = "UPDATE " . $this->_table . " SET rght=rght+2 WHERE rght>=" . $next_right;
	  $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  $sql = "UPDATE " . $this->_table . " SET lft=lft+2 WHERE lft>=" . $next_right;	
	  $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  
	  $id = $this->_db->GenID("sequences");
	  $sql = "INSERT INTO " . $this->_table . " SET name='" . $name . "', value='" . $value . "', lft='" . $next_right . "', rght='" . ($next_right + 1) . "', parent='" . $parent_id . "', id='" . $id . "'";
	  $this->_db->Execute($sql) or die("Error: $sql :: " . $this->_db->ErrorMsg());
	  //$this->rebuild_tree(1,1);
	  $this->load_tree();
	  return $id;
	}
	
	/*
	*	Call this to delete a node from the tree, the nodes children (and their children, etc) will become children
	*	of the deleted nodes parent
	*	@param int $id id of the node you want to delete
	*/
	function delete_node($id) {
   	  
	  $sql = "SELECT * from " . $this->_table . " where id = '" . $id . "'";
	  //echo $sql . "<br>";
	  $result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
   
   	  $left = 0;
   	  $right = 1;
   	  $new_parent = 0;
   	  
	  if ($result && !$result->EOF) {
	    $left = $result->fields['lft'];
	    $right = $result->fields['rght'];
	    $new_parent = $result->fields['parent']; 
	  }
   
	  $sql = "UPDATE " . $this->_table . " SET rght=rght-2 WHERE rght>" . $right;
	  //echo $sql . "<br>";
	  $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  
	   $sql = "UPDATE " . $this->_table . " SET lft=lft-2 WHERE lft>" . $right;
	  //echo $sql . "<br>";
	  $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  
	  $sql = "UPDATE " . $this->_table . " SET lft=lft-1, rght=rght-1 WHERE lft>" . $left . " and rght < " . $right;	
	  //echo $sql . "<br>";
	  $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
   
	  //only update the childrens parent setting if the node has children
	  if ($right > ($left +1)) {
	    $sql = "UPDATE " . $this->_table . " SET parent='" . $new_parent . "' WHERE parent='" . $id . "'";
	    //echo $sql . "<br>";	
	    $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  }
	  
	  $sql = "DELETE FROM " . $this->_table . " where id='" . $id . "'";
	  //echo $sql . "<br>";
	  $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  $this->load_tree();
	  
	  return true;
	}
	
	function rename_node($node_id, $name) {
		$sql = "SELECT * from " . $this->_table . " where name=" . $this->_db->qstr($name)  . " and id != " . $this->_db->qstr($node_id);
	  	$result = $this->_db->Execute($sql) or die("Error: " . $this->_db->ErrorMsg());
	  
	  	if ($result && !$result->EOF) {
	  		die("You cannot rename with that name as a node with the name '" . $name ."' already exists. <br>");
	  	}
	  	
	  	$sql = "UPDATE " . $this->_table . " set name =" . $this->_db->qstr($name) . " WHERE id=" . $this->_db->qstr($node_id);
	  	$res = $this->_db->Execute($sql);
	  	$this->load_tree();
	}
	
	function get_node_info($id) {
		if(!empty($this->_id_name[$id])) {
			return $this->_id_name[$id];
		}
		else {
			return array();	
		}
	}
	
	function get_node_name($id) {
		if(!empty($this->_id_name[$id])) {
			return $this->_id_name[$id]['name'];
		}
		else {
			return false;	
		}
	} 
	
	function get_sibling_nodes($id) {
		$node_info = $this->get_node_info($id);
		$parent_id = $node_info['parent'];
		$sql = "SELECT id from " . $this->_table . " where parent = " . $this->_db->qstr($parent_id);
		$res = $this->_db->Execute($sql);
		$ids = array();
		while ($res && !$res->EOF) {
			$ids[] = $res->fields['id'];	
			$res->MoveNext();
		}
		return $ids;	
	}  
	
	function get_children_nodes($id) {
		$sql = "SELECT id from " . $this->_table . " where parent = " . $this->_db->qstr($id);
		$res = $this->_db->Execute($sql);
		$ids = array();
		while ($res && !$res->EOF) {
			$ids[] = $res->fields['id'];
			$res->MoveNext();
		}
		return $ids;	
	}
	
	function  get_parent_nodes($id) {
		$node_info = $this->get_node_info($id);
		$parent_id = $node_info['parent'];
		return $this->get_sibling_nodes($parent_id);
	}

}

function array_merge_2(&$array, &$array_i) {
       // For each element of the array (key => value):
       foreach ($array_i as $k => $v) {
           // If the value itself is an array, the process repeats recursively:
           if (is_array($v)) {
               if (!isset($array[$k])) {
                   $array[$k] = array();
               }
               array_merge_2($array[$k], $v);

           // Else, the value is assigned to the current element of the resulting array:
           } else {
               if (isset($array[$k]) && is_array($array[$k])) {
                   $array[$k][0] = $v;
               } else {
                   if (isset($array) && !is_array($array)) {
                       $temp = $array;
                       $array = array();
                       $array[0] = $temp;
                   }
                   $array[$k] = $v;
               }
           }
       }
   }


   function array_merge_n() {
       // Initialization of the resulting array:
       $array = array();

       // Arrays to be merged (function's arguments):
       $arrays = func_get_args();

       // Merging of each array with the resulting one:
       foreach ($arrays as $array_i) {
           if (is_array($array_i)) {
               array_merge_2($array, $array_i);
           }
       }

       return $array;
   }


/*
$db = $GLOBALS['adodb']['db'];
$sql = "USE document;";
$db->Execute($sql);
$t = new Tree(1);
echo "<pre>";
print_r($t->tree);
//$nid = $t->add_node(0,"test node2","test value");
//$t->add_node($nid,"test child","another value");
//$t->add_node($nid,"test child");
print_r($t->tree);
echo "</pre>";
*/
?>
